/*
 * Copyright 2007 Agile Plugins Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.agileplugins.refactor;

import com.intellij.codeInsight.highlighting.HighlightManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataConstants;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassObjectAccessExpression;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionList;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiIdentifier;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Query;

import javax.swing.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class InlineParameterAction extends AnAction {
    private Logger logger = Logger.getInstance(getClass().getName());
    private List<PsiElement> elementsToDelete = new ArrayList<PsiElement>();
    private PsiMethod method;
    private String parameterName;
    private AnAction inlineAction;
    private String selectedReplacementExpression;

    public InlineParameterAction() {
        super();
    }

    public InlineParameterAction(AnAction inlineAction) {
        this.inlineAction = inlineAction;
    }

    private void a() {
        b("3", "5");
    }

    private void b(String a, String b) {
        c(a + "+" + b);
    }

    private void c(String expression) {
        System.out.println("expression = " + expression);
    }

    public String getText() {
        return "Inline Parameter '" + parameterName + "'";
    }

    public void actionPerformed(AnActionEvent anActionEvent) {
        if (!tryProcess(anActionEvent) && inlineAction != null) {
            inlineAction.actionPerformed(anActionEvent);
        }
    }

    private boolean tryProcess(AnActionEvent anActionEvent) {
        DataContext datacontext = anActionEvent.getDataContext();
        final Project project = (Project) datacontext.getData(DataConstants.PROJECT);
        if (project == null)
            return false;
        PsiDocumentManager.getInstance(project).commitAllDocuments();
        final Editor editor = (Editor) datacontext.getData(DataConstants.EDITOR);
        if (editor != null) {
            final PsiFile psifile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
            if (psifile == null)
                return false;
            PsiParameter parameter = getParameter(editor, psifile);
            if (parameter == null) {
                return false;
            }
            inlineParameter(editor, parameter, psifile);
            return true;
        }
        return false;
    }

    private void inlineParameter(final Editor editor, PsiParameter param, final PsiFile psiFile) {
        final Project project = param.getProject();
        parameterName = param.getName();
        PsiClass thisClass = PsiUtil.getContainingClass(param);
        PsiElement parent = param.getParent();
        if (!(parent instanceof PsiParameterList)) {
            return;
        }
        PsiParameterList list = (PsiParameterList) parent;
        method = (PsiMethod) list.getParent();
        Query<PsiReference> query = ReferencesSearch.search(method, method.getUseScope(), true);
        Collection<PsiReference> all = query.findAll();
        int referencesCount = all.size();
        if (referencesCount == 0) {
            errorMessage("Can't inline parameter " + parameterName +
                    " as method " + method.getName() + " is not called.", project);
            return;
        }
        elementsToDelete.clear();
        PsiParameter[] parameters = list.getParameters();
        int parameterIndex = list.getParameterIndex(param);
        Set<String> replacementExpressions = new HashSet<String>();
        EditorColorsManager editorcolorsmanager = EditorColorsManager.getInstance();
        TextAttributes attributes = editorcolorsmanager.getGlobalScheme().getAttributes(
                EditorColors.SEARCH_RESULT_ATTRIBUTES);
        for (PsiReference reference : all) {
            try {
                PsiElement element = reference.getElement();
                PsiElement parent2 = element.getParent();
                if (!(parent2 instanceof PsiMethodCallExpression)) {
                    return;
                }
                PsiMethodCallExpression exp = (PsiMethodCallExpression) parent2;
                PsiExpressionList argumentList = exp.getArgumentList();
                PsiExpression[] expressions = argumentList.getExpressions();
                if (expressions.length <= parameterIndex) {
                    return;
                }

                PsiExpression exp2 = expressions[parameterIndex];
                InlineParameterHelper helper = new InlineParameterHelper(thisClass, parameters,
                        expressions, parameterIndex);
                PsiExpression psiElement = (PsiExpression) helper.process(exp2.copy());
                replacementExpressions.add(psiElement.getText());
                elementsToDelete.add(exp2);
            }
            catch (CantInlineException e) {
                HighlightManager highlightManager = HighlightManager.getInstance(project);
                highlightManager.addOccurrenceHighlights(editor, new PsiElement[]{
                        e.getPsiElement()}, attributes, true, null);
                errorMessage("Can't inline parameter " + param.getName() + " - " + e.getMessage(), project);
                WindowManager.getInstance().getStatusBar(project).setInfo("Press Escape to remove the highlighting");
                return;
            }
        }
        elementsToDelete.add(param);

        PsiElement[] elementsToHighlight = elementsToDelete.toArray(new PsiElement[0]);

        HighlightManager highlightManager = HighlightManager.getInstance(project);
        highlightManager.addOccurrenceHighlights(editor, elementsToHighlight, attributes, true, null);

        String message;
        String[] options = null;
        if (replacementExpressions.size() == 1) {
            selectedReplacementExpression = replacementExpressions.iterator().next();
            message = "Inline parameter " + parameterName + " with the expression '" +
                    selectedReplacementExpression + "' (" +
                    referencesCount + (referencesCount != 1 ? " occurrences)" : " occurrence)") + "?";
        } else {
            message = "Parameter " + parameterName +
                    " has multiple expressions.  Please choose the expression to inline.";
            options = replacementExpressions.toArray(new String[0]);
        }
        MessageDialog dialog = new MessageDialog(
                "Inline Variable", message,
                "refactoring.inlineParameter", "OptionPane.questionIcon", options, true, project, ListSelectionModel.SINGLE_SELECTION);
        dialog.show();
        if (!dialog.isOK()) {
            WindowManager.getInstance().getStatusBar(project).setInfo("Press Escape to remove the highlighting");
            return;
        }
        if (replacementExpressions.size() > 1) {
            selectedReplacementExpression = dialog.getSelectedListOption();
            if (selectedReplacementExpression == null || selectedReplacementExpression.length() == 0) {
                return;
            }
        }
        final Runnable runnable = new Runnable() {
            public void run() {
                try {
                    invoke(project, editor, psiFile);
                } catch (IncorrectOperationException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        CommandProcessor.getInstance().executeCommand(project,
                new Runnable() {
                    public void run() {
                        ApplicationManager.getApplication().runWriteAction(runnable);
                    }
                }, "Inline " + parameterName, null);
    }

    private void errorMessage(String message, Project project) {
        MessageDialog messageDialog = new MessageDialog("Inline Variable",
                message, "refactoring.inlineVariable", "OptionPane.errorIcon", null, false, project, ListSelectionModel.SINGLE_SELECTION);
        messageDialog.show();
    }

    protected PsiParameter getParameter(Editor editor, PsiFile psiFile) {
        PsiElement element = getElement(editor, psiFile);
        if (element instanceof PsiIdentifier) {
            element = element.getParent();
        } else if (element != null) {
            element = element.getPrevSibling();
        }
        if (element instanceof PsiParameter) {
            return (PsiParameter) element;
        }
        return null;
    }

    protected PsiElement getElement(Editor editor, PsiFile psiFile) {
        int offset = editor.getCaretModel().getOffset();
        return psiFile.findElementAt(offset);
    }


    public void invoke(Project project, Editor editor, PsiFile psiFile) throws IncorrectOperationException {
        PsiParameter param = getParameter(editor, psiFile);
        PsiManager manager = method.getManager();
        PsiElementFactory elementFactory = manager.getElementFactory();
        Query<PsiReference> query = ReferencesSearch.search(param, param.getUseScope(), true);
        Collection<PsiReference> all = query.findAll();
        PsiExpression replacementExpression = elementFactory.createExpressionFromText(
                selectedReplacementExpression, method);
        if (shouldInlineReplacement(replacementExpression, all.size())) {
            for (PsiReference reference : all) {
                PsiElement element = reference.getElement();
                element.replace(replacementExpression.copy());
            }
        } else {
            String replacementStatement = param.getType().getPresentableText() + " " +
                    param.getName() + " = " + selectedReplacementExpression + ";";
            PsiStatement statement = elementFactory.createStatementFromText(replacementStatement, method);
            method.getBody().addAfter(statement, method.getBody().getLBrace());
        }

        logger.info("deleting elements");
        for (PsiElement psiElement : elementsToDelete) {
            logger.info("elementToDelete = " + psiElement);
            PsiMethod method = getContainingMethod(psiElement);
            psiElement.delete();
            PsiStatement[] statements = method.getBody().getStatements();
            if (psiElement instanceof PsiExpression) {
                for (int i = statements.length - 1; i >= 0; i--) {
                    if (statements[i] instanceof PsiDeclarationStatement) {
                        PsiDeclarationStatement dec = (PsiDeclarationStatement) statements[i];
                        PsiElement[] declaredElements = dec.getDeclaredElements();
                        if (declaredElements.length == 1 && declaredElements[0] instanceof PsiLocalVariable) {
                            PsiLocalVariable psiLocalVariable = (PsiLocalVariable) declaredElements[0];
                            Query<PsiReference> localRefs = ReferencesSearch.search(
                                    psiLocalVariable, psiLocalVariable.getUseScope(), true);
                            if (localRefs == null || localRefs.findAll().size() == 0) {
                                psiLocalVariable.delete();
                            }
                        }
                    }
                }
            }
        }
    }

    private boolean shouldInlineReplacement(PsiExpression replacementExpression, int references) {
        return references == 1 || (references > 1 &&
                (replacementExpression instanceof PsiClassObjectAccessExpression ||
                        replacementExpression instanceof PsiReferenceExpression));
    }

    public PsiMethod getContainingMethod(PsiElement element) {
        while (element != null && !(element instanceof PsiMethod)) {
            element = element.getParent();
        }
        return (PsiMethod) element;

    }

}
